﻿// 文件使用MiniExcel源码， 使用时需遵循其 Apache License 2.0
// 修改摘要通知： 删除异步、Task等net2.0中不支持的内容，替换原先C#自带的zip处理方式，改用yacomp 进行解压缩。
using ExcelConfig.Excel.SaveByTemplate;
using ExcelConfig.Utils;
using ExcelConfig.Zip;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using yacomp.os;

namespace ExcelConfig.OpenXml.SaveByTemplate
{
    internal partial class ExcelOpenXmlTemplate : IExcelTemplate
    {
        private static readonly Regex _isExpressionRegex = new Regex("(?<={{).*?(?=}})");
        private static readonly XmlNamespaceManager _ns;

        private readonly Stream _outputFileStream;
        private readonly OpenXmlConfiguration _configuration;
        private readonly IInputValueExtractor _inputValueExtractor;
        private readonly StringBuilder _calcChainContent = new StringBuilder();

        static ExcelOpenXmlTemplate()
        {
            _ns = new XmlNamespaceManager(new NameTable());
            _ns.AddNamespace("x", Config.SpreadsheetmlXmlns);
            _ns.AddNamespace("x14ac", Config.SpreadsheetmlXml_x14ac);
        }

        public ExcelOpenXmlTemplate(Stream stream, IConfiguration configuration, InputValueExtractor inputValueExtractor)
        {
            _outputFileStream = stream;
            _configuration = (OpenXmlConfiguration)configuration ?? OpenXmlConfiguration.DefaultConfig;
            _inputValueExtractor = inputValueExtractor;
        }

        public void SaveAsByTemplate(string templatePath, object value)
        {
            using (var stream = FileHelper.OpenSharedRead(templatePath))
                SaveAsByTemplateImpl(stream, value);
        }
        public void SaveAsByTemplate(Stream template_stream, object value)
        {
            SaveAsByTemplateImpl(template_stream, value);
        }

        public void SaveAsByTemplate(byte[] templateBtyes, object value)
        {
            using (Stream stream = new MemoryStream(templateBtyes))
                SaveAsByTemplateImpl(stream, value);
        }

        public void SaveAsByTemplate(Stream s, ReplaceSheetDto value)
        {
            SaveAsByTemplateImpl2(s, value);
        }
        internal void SaveAsByTemplateImpl(Stream templateStream, object value)
        {
            //only support xlsx
            //templateStream.CopyTo(_outputFileStream);

            // foreach all templateStream and create file for _outputFileStream and not create sheet file
            templateStream.Position = 0;
            var templateReader = new ExcelOpenXmlSheetReader(templateStream, null);
            var outputFileArchive = new ExcelOpenXmlZip(_outputFileStream, true, Encoding.UTF8, isUpdateMode: false);
            try
            {
                outputFileArchive.storage = outputFileArchive.zipFile.storage; //TODO:need to remove
            }
            catch (InvalidDataException e)
            {
                throw new InvalidDataException($"An invalid valid OpenXml zip archive was detected, please check or open an issue for this error: {e.Message}");
            }

            templateStream.Position = 0;
            // Create a new zip file for writing
            //using (FileStream newZipStream = new FileStream(newZipPath, FileMode.Create))
            //using (ZipArchive newArchive = new ZipArchive(_outputFileStream, ZipArchiveMode.Create))

            // Iterate through each entry in the original archive
            foreach (YacompFile entry in templateReader._archive.storage.LoopFiles())
            {
                if (entry.FullPath.StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) ||
                    entry.FullPath.StartsWith("/xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) ||
                    entry.FullPath.Contains("xl/calcChain.xml")
                )
                    continue;

                // Create a new entry in the new archive with the same name
                var newEntry = outputFileArchive.storage.AddFilebyPath(entry.FullPath, entry.ToStream());
            }

            //read sharedString
            var templateSharedStrings = templateReader._sharedStrings;
            templateStream.Position = 0;

            //read all xlsx sheets
            int sheetIdx = 0;
            foreach (var templateSheet in templateReader._archive.zipFile.storage.LoopFiles())
            {
                if (!(templateSheet.FullPath.StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) ||
                    templateSheet.FullPath.StartsWith("/xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase))) continue;
                //every time need to use new XRowInfos or it'll cause duplicate problem: https://user-images.githubusercontent.com/12729184/115003101-0fcab700-9ed8-11eb-9151-ca4d7b86d59e.png
                _xRowInfos = new List<XRowInfo>();
                _xMergeCellInfos = new Dictionary<string, XMergeCell>();
                _newXMergeCellInfos = new List<XMergeCell>();

                var outputZipSheetEntryStream = new MemoryStream();
                var templateFullName = templateSheet.FullPath;
                var inputValues = _inputValueExtractor.ToValueDictionary(value);
                GenerateSheetXmlImplByCreateMode(templateSheet, outputZipSheetEntryStream, inputValues, templateSharedStrings, false);
                //doc.Save(zipStream); //don't do it because: https://user-images.githubusercontent.com/12729184/114361127-61a5d100-9ba8-11eb-9bb9-34f076ee28a2.png
                // disposing writer disposes streams as well. read and parse calc functions before that
                outputFileArchive.storage.AddFilebyPath(templateFullName, outputZipSheetEntryStream);
                sheetIdx++;
                _calcChainContent.Append(CalcChainHelper.GetCalcChainContent(_calcChainCellRefs, sheetIdx));
            }

            // create mode we need to not create first then create here
            if (outputFileArchive.storage.Exist("xl/calcChain.xml"))
            {
                var calcChainPathName = "xl/calcChain.xml";
                //calcChain.Delete();

                var calcChainStream = new MemoryStream();
                CalcChainHelper.GenerateCalcChainSheet(calcChainStream, _calcChainContent.ToString());
                outputFileArchive.storage.AddFilebyPath(calcChainPathName, calcChainStream);
            }
            else
            {
                if (templateReader._archive.storage.Exist("xl/calcChain.xml"))
                {
                    var _calcChain = templateReader._archive.storage.FindFile("xl/calcChain.xml");
                    var newEntry = outputFileArchive.storage.AddFilebyPath(_calcChain.FullPath, _calcChain.ToStream());
                }
            }
            outputFileArchive.zipFile.Save();
            outputFileArchive.zipFile.Dispose();
        }

        internal void SaveAsByTemplateImpl2(Stream templateStream, ReplaceSheetDto value)
        {
            //only support xlsx
            //templateStream.CopyTo(_outputFileStream);

            // foreach all templateStream and create file for _outputFileStream and not create sheet file
            templateStream.Position = 0;
            var templateReader = new ExcelOpenXmlSheetReader(templateStream, null);
            var outputFileArchive = new ExcelOpenXmlZip(_outputFileStream, true, Encoding.UTF8, isUpdateMode: false);
            try
            {
                outputFileArchive.storage = outputFileArchive.zipFile.storage; //TODO:need to remove
            }
            catch (InvalidDataException e)
            {
                throw new InvalidDataException($"An invalid valid OpenXml zip archive was detected, please check or open an issue for this error: {e.Message}");
            }

            templateStream.Position = 0;
            // Create a new zip file for writing
            //using (FileStream newZipStream = new FileStream(newZipPath, FileMode.Create))
            //using (ZipArchive newArchive = new ZipArchive(_outputFileStream, ZipArchiveMode.Create))

            // Iterate through each entry in the original archive
            foreach (YacompFile entry in templateReader._archive.storage.LoopFiles())
            {
                if (entry.FullPath.StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) ||
                    entry.FullPath.StartsWith("/xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) ||
                    entry.FullPath.Contains("xl/calcChain.xml")
                )
                    continue;

                // Create a new entry in the new archive with the same name
                var newEntry = outputFileArchive.storage.AddFilebyPath(entry.FullPath, entry.ToStream());
            }

            //read sharedString
            var templateSharedStrings = templateReader._sharedStrings;
            templateStream.Position = 0;

            //read all xlsx sheets
            int sheetIdx = 0;
            var book = templateReader.GetWorkbookRels(templateReader._archive.storage);
            var replace_sheetrecord = book.Find(t => t.Name == value.SheetName);
            foreach (var templateSheet in templateReader._archive.zipFile.storage.LoopFiles())
            {
                if (!(templateSheet.FullPath.StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) ||
                    templateSheet.FullPath.StartsWith("/xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase))) continue;
                //every time need to use new XRowInfos or it'll cause duplicate problem: https://user-images.githubusercontent.com/12729184/115003101-0fcab700-9ed8-11eb-9151-ca4d7b86d59e.png
                _xRowInfos = new List<XRowInfo>();
                _xMergeCellInfos = new Dictionary<string, XMergeCell>();
                _newXMergeCellInfos = new List<XMergeCell>();

                var outputZipSheetEntryStream = new MemoryStream();
                var templateFullName = templateSheet.FullPath;
                if (replace_sheetrecord != null && "xl/"+replace_sheetrecord.Path == templateSheet.FullPath)
                    GenerateSheetXmlImplBySheetMode(templateSheet, outputZipSheetEntryStream, value, templateSharedStrings, false);
                else
                    GenerateSheetXmlImplBySheetMode(templateSheet, outputZipSheetEntryStream, null, templateSharedStrings, false);
                //doc.Save(zipStream); //don't do it because: https://user-images.githubusercontent.com/12729184/114361127-61a5d100-9ba8-11eb-9bb9-34f076ee28a2.png
                // disposing writer disposes streams as well. read and parse calc functions before that
                outputFileArchive.storage.AddFilebyPath(templateFullName, outputZipSheetEntryStream);
                sheetIdx++;
                _calcChainContent.Append(CalcChainHelper.GetCalcChainContent(_calcChainCellRefs, sheetIdx));
            }

            // create mode we need to not create first then create here
            if (outputFileArchive.storage.Exist("xl/calcChain.xml"))
            {
                var calcChainPathName = "xl/calcChain.xml";
                //calcChain.Delete();

                var calcChainStream = new MemoryStream();
                CalcChainHelper.GenerateCalcChainSheet(calcChainStream, _calcChainContent.ToString());
                outputFileArchive.storage.AddFilebyPath(calcChainPathName, calcChainStream);
            }
            else
            {
                if (templateReader._archive.storage.Exist("xl/calcChain.xml"))
                {
                    var _calcChain = templateReader._archive.storage.FindFile("xl/calcChain.xml");
                    var newEntry = outputFileArchive.storage.AddFilebyPath(_calcChain.FullPath, _calcChain.ToStream());
                }
            }
            outputFileArchive.zipFile.Save();
            outputFileArchive.zipFile.Dispose();
        }
    }
}